package simple.utils.data;

import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Created with IntelliJ IDEA.
 * User: rehak
 * Date: 5/10/13
 * Time: 4:10 PM
 * To change this template use File | Settings | File Templates.
 */
public class MultiMappedFile extends InputStream implements Closeable {

    private static final Logger LOGGER = LoggerFactory.getLogger(MultiMappedFile.class);
    public static final long MAX_BUF_SIZE = Integer.MAX_VALUE - 8;
    //public static final long MAX_BUF_SIZE = 10;
    private final FileChannel fileChannel;
    private final MappedByteBuffer[] bufs;
    private volatile int currentBuf = 0;
    private volatile long gotInThisBuf = 0;
    private final byte[] buf = new byte[1];

    public MultiMappedFile(File f) throws IOException {
        Preconditions.checkNotNull(f);
        this.fileChannel = new RandomAccessFile(f.getAbsolutePath(), "rw").getChannel();
        // map
        // how many bufs do we need?
        long fileSize = fileChannel.size();
        int cnt = (int) (fileSize / MAX_BUF_SIZE);
        if (fileSize % MAX_BUF_SIZE != 0) {
            cnt++;
        }
        LOGGER.debug("Mapping " + fileSize + " bytes long file to " + cnt + " mapped buffers");
        long remaining = fileSize;
        long pos = 0;
        this.bufs = new MappedByteBuffer[cnt];
        for (int i = 0; i < cnt; i++) {
            int length = (int) Math.min(MAX_BUF_SIZE, remaining);
            LOGGER.debug("Mapping next MappedByteBuffer to position " + pos + ", length " + length);
            bufs[i] = fileChannel.map(FileChannel.MapMode.READ_ONLY, pos, length);
            remaining -= length;
            pos += length;
        }
    }

    public void reset() {
        currentBuf = 0;
        gotInThisBuf = 0;
    }

    public MappedByteBuffer[] getBufs() {
        return bufs;
    }

    public int available() {
        if (currentBuf >= bufs.length) {
            // reached end
            return -1;
        } else {
            return 1;
        }
    }

    @Override
    public int read() throws IOException {
        if (available() < 0) {
            // end of stream
            return -1;
        } else {
            read(buf);
            return buf[0] & 0xFF;
        }
    }

    public int read(byte[] buffer) {
        if (currentBuf >= bufs.length) {
            // reached end
            return -1;
        }
        MappedByteBuffer buf = bufs[currentBuf];
        int remains = buf.remaining();
        int willRead = Math.min(remains, buffer.length);
        buf.get(buffer, 0, willRead);
        gotInThisBuf += willRead;
        if (gotInThisBuf >= buf.capacity()) {
            LOGGER.debug("Going to next buffer [" + (currentBuf + 1) + "/" + bufs.length + "]");
            gotInThisBuf = 0;
            currentBuf++;
        }
        return willRead;
    }

    public void write(byte[] data) throws IOException {
        fileChannel.write(ByteBuffer.wrap(data));
    }

    @Override
    public void close() throws IOException {
        for (MappedByteBuffer b : bufs) {
            b.force();
        }
        fileChannel.close();
    }


    public long size() throws IOException {
        return fileChannel.size();
    }

    public void load() {
        for (MappedByteBuffer mb : bufs) {
            mb.load();
        }
    }

    public static LargeBytes load(File file) throws IOException {
        MultiMappedFile mmf = new MultiMappedFile(file);
        MappedByteBuffer[] bufs = mmf.getBufs();
        LOGGER.info("Loading file to memory, total " + bufs.length + " segments");
        byte[][] data = new byte[bufs.length][];
        for (int i = 0; i < bufs.length; i++) {
            MappedByteBuffer b = bufs[i];
            byte[] ar = new byte[b.limit()];
            b.get(ar);
            data[i] = ar;
            LOGGER.info("Segment " + (i + 1) + " loaded!");
        }
        mmf.close();
        LOGGER.info("File loaded!");
        return new LargeBytes(data);
    }

    /**
     * Get buffer of bytes from index
     *
     * @param index  index
     * @param buffer byte array to fill
     */
    public void get(long index, byte[] buffer) throws IOException {
        long end = index + buffer.length;
        int startBuf = inBuf(index);
        int endBuf = inBuf(end);
        if (startBuf == -1 || endBuf == -1) {
            throw new IOException("Out of range - (" + index + "," + end + ")");
        }
        if (startBuf == endBuf) {
            // read all from first buffer
            read(startBuf, index, buffer, 0, buffer.length);
        } else {
            // read part from first buffer
            int fromFirst = (int) (MAX_BUF_SIZE - (index % MAX_BUF_SIZE));
            read(startBuf, index, buffer, 0, fromFirst);
            // rest from last buffer
            read(endBuf, 0, buffer, fromFirst, buffer.length - fromFirst);
        }
    }

    private void read(int bufIndex, long absoluteIndex, byte[] buffer, int offset, int readSize) {
        int bufStart = (int) (absoluteIndex % MAX_BUF_SIZE);
        MappedByteBuffer b = bufs[bufIndex];
        b.position(bufStart);
        b.get(buffer, offset, readSize);
    }

    private int inBuf(long index) {
        int bufIndex = (int) (index / MAX_BUF_SIZE);
        if (bufIndex >= bufs.length) {
            return -1;
        }
        return bufIndex;
    }
}